﻿
namespace Patterns4Net.Core.CecilExtensions
{
    using System.Linq;
    using System.Runtime.CompilerServices;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using Mono.Cecil.Rocks;

    public static class PropertyExtensions
    {
        public static bool IsAutoImplemented(this PropertyDefinition property)
        {
            return GetAutoImplementedBackingField(property) != null;
        }

        /// <summary>
        /// It the field is backing field of some property, this method return a definition of that property, 
        /// otherwise <c>null</c>.
        /// </summary>
        public static PropertyDefinition GetProperty(this FieldDefinition field)
        {
            return field.DeclaringType.Properties
                .FirstOrDefault(p =>
                {
                    var autoField = p.GetAutoImplementedBackingField();
                    return
                        autoField != null &&
                        autoField.Name == field.Name;
                });            
        }

        /// <summary>
        /// For auto implemented properties returns backing field generated by compiler, for other properties return <c>null</c>.
        /// </summary>
        /// <param name="property">The property</param>
        /// <returns>Backing field reference or null.</returns>
        public static FieldDefinition GetAutoImplementedBackingField(this PropertyDefinition property)
        {
            if (property.GetMethod == null ||
                property.SetMethod == null)
            {
                return null;
            }

            property.GetMethod.Body.SimplifyMacros();
            var loadInstr = property.GetMethod.Body.Instructions.FirstOrDefault(x => x.OpCode == OpCodes.Ldfld);
            if (loadInstr == null)
            {
                return null;
            }

            /* here we deliberately do not check that the fields are actually fields of 'this' object */

            var field = loadInstr.Operand as FieldDefinition;
            if (field.HasAttribute<CompilerGeneratedAttribute>() == false)
            {
                return null;
            }

            property.SetMethod.Body.SimplifyMacros();
            var stroreInstr = property.SetMethod.Body.Instructions.FirstOrDefault(x => x.OpCode == OpCodes.Stfld);
            if (stroreInstr == null)
            {
                return null;
            }

            var setField = stroreInstr.Operand as FieldDefinition;
            if (setField == null ||
                setField.Name != field.Name)
            {
                return null;
            }

            return field;
        }

        /// <summary>
        /// Returns <c>true</c> when the field has an attribute, otherwise <c>false</c>.
        /// In case of fields that are backing fields of auto-implemented properties, 
        /// it also checks whether the property has the attribute.
        /// </summary>
        /// <typeparam name="TAttribute">Attribute type</typeparam>
        /// <param name="field">The field</param>
        public static bool HasAttribute<TAttribute>(this FieldDefinition field)
        {
            if (MemberExtensions.HasAttribute<TAttribute>(field))
            {
                return true;
            }

            if (MemberExtensions.HasAttribute<CompilerGeneratedAttribute>(field) == false)
            {
                // if it is not compiler generated, than it is not backing field of auto implemented property
                return false;
            }

            return field.DeclaringType.Properties
                .Any(p =>
                    {
                        var autoField = p.GetAutoImplementedBackingField();
                        return 
                            p.HasAttribute<TAttribute>() &&
                            autoField != null && 
                            autoField.Name == field.Name;
                    });
        }
    }
}
